/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 *
 */
package org.apache.asyncweb.common;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.CharacterCodingException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TimeZone;

import org.apache.asyncweb.common.codec.HttpCodecUtils;
import org.apache.mina.core.buffer.IoBuffer;

/**
 * A default implementation of {@link MutableHttpRequest}.
 * 
 * @author The Apache MINA Project (dev@mina.apache.org)
 */
public class DefaultHttpRequest extends DefaultHttpMessage implements
		MutableHttpRequest {

	private static final long serialVersionUID = 3044997961372568928L;

	private HttpMethod method = HttpMethod.GET;
	private URI requestUri;
	private Map<String, List<String>> parameters = new HashMap<String, List<String>>();

	/**
	 * Creates a new instance.
	 */
	public DefaultHttpRequest() {
	}

	public void setCookies(String headerValue) {
		clearCookies();

		int version = -1; // -1 means version is not parsed yet.
		int fieldIdx = 0;
		MutableCookie currentCookie = null;

		StringTokenizer tk = new StringTokenizer(headerValue, ";,");

		while (tk.hasMoreTokens()) {
			String pair = tk.nextToken();
			String key;
			String value;

			int equalsPos = pair.indexOf('=');
			if (equalsPos >= 0) {
				key = pair.substring(0, equalsPos).trim();
				value = pair.substring(equalsPos + 1).trim();
			} else {
				key = pair.trim();
				value = "";
			}

			if (version < 0) {
				if (!key.equalsIgnoreCase("$Version")) {
					// $Version is not specified. Use the default (0).
					version = 0;
				} else {
					version = Integer.parseInt(value);
					if (version != 0 && version != 1) {
						throw new IllegalArgumentException("Invalid version: "
								+ version + " (" + headerValue + ")");
					}
				}
			}

			if (version >= 0) {
				try {
					switch (fieldIdx) {
					case 1:
						if (key.equalsIgnoreCase("$Path")) {
							currentCookie.setPath(value);
							fieldIdx++;
						} else {
							fieldIdx = 0;
						}
						break;
					case 2:
						if (key.equalsIgnoreCase("$Domain")) {
							currentCookie.setDomain(value);
							fieldIdx++;
						} else {
							fieldIdx = 0;
						}
						break;
					case 3:
						if (key.equalsIgnoreCase("$Port")) {
							// Ignoring for now
							fieldIdx++;
						} else {
							fieldIdx = 0;
						}
						break;
					}
				} catch (NullPointerException e) {
					throw new IllegalArgumentException(
							"Cookie key-value pair not found (" + headerValue
									+ ")");
				}

				if (fieldIdx == 0) {
					currentCookie = new DefaultCookie(key);
					currentCookie.setVersion(version);
					currentCookie.setValue(value);
					addCookie(currentCookie);
					fieldIdx++;
				}
			}
		}
	}

	public URI getRequestUri() {
		return requestUri;
	}

	public void setRequestUri(URI requestUri) {
		if (requestUri == null) {
			throw new NullPointerException("requestUri");
		}
		this.requestUri = requestUri;
	}

	public boolean requiresContinuationResponse() {
		if (getProtocolVersion() == HttpVersion.HTTP_1_1) {
			String expectations = getHeader(HttpHeaderConstants.KEY_EXPECT);
			if (expectations != null) {
				return expectations
						.indexOf(HttpHeaderConstants.VALUE_CONTINUE_EXPECTATION) >= 0;
			}
		}
		return false;
	}

	public HttpMethod getMethod() {
		return method;
	}

	public void setMethod(HttpMethod method) {
		if (method == null) {
			throw new NullPointerException("method");
		}
		this.method = method;
	}

	public void addParameter(String name, String value) {
		List<String> values = parameters.get(name);
		if (values == null) {
			values = new ArrayList<String>();
			parameters.put(name, values);
		}
		values.add(value);
	}

	public boolean removeParameter(String name) {
		return parameters.remove(name) != null;
	}

	public void setParameter(String name, String value) {
		List<String> values = new ArrayList<String>();
		values.add(value);
		parameters.put(name, values);
	}

	public void setParameters(Map<String, List<String>> parameters) {
		clearParameters();

		for (Map.Entry<String, List<String>> entry : parameters.entrySet()) {
			for (String value : entry.getValue()) {
				if (value == null) {
					throw new NullPointerException("Parameter '"
							+ entry.getKey() + "' contains null.");
				}
			}
			if (entry.getValue().size() > 0) {
				this.parameters.put(entry.getKey(), entry.getValue());
			}
		}
	}

	public void setParameters(String queryString) {
		try {
			this.setParameters(queryString, HttpCodecUtils.DEFAULT_CHARSET_NAME);
		} catch (UnsupportedEncodingException e) {
			throw new InternalError(HttpCodecUtils.DEFAULT_CHARSET_NAME
					+ " decoder must be provided by JDK.");
		}
	}

	public void setParameters(String queryString, String encoding)
			throws UnsupportedEncodingException {
		clearParameters();

		if (queryString == null || queryString.length() == 0) {
			return;
		}

		int pos = 0;
		while (pos < queryString.length()) {
			int ampPos = queryString.indexOf('&', pos);

			String value;
			if (ampPos < 0) {
				value = queryString.substring(pos);
				ampPos = queryString.length();
			} else {
				value = queryString.substring(pos, ampPos);
			}

			int equalPos = value.indexOf('=');
			if (equalPos < 0) {
				this.addParameter(URLDecoder.decode(value, encoding), "");
			} else {
				this.addParameter(URLDecoder.decode(
						value.substring(0, equalPos), encoding), URLDecoder
						.decode(value.substring(equalPos + 1), encoding));
			}

			pos = ampPos + 1;
		}
	}

	public void clearParameters() {
		this.parameters.clear();
	}

	public boolean containsParameter(String name) {
		return parameters.containsKey(name);
	}

	public String getParameter(String name) {
		List<String> values = parameters.get(name);
		if (values == null) {
			return null;
		}

		return values.get(0);
	}

	public Map<String, List<String>> getParameters() {
		return Collections.unmodifiableMap(parameters);
	}

	@Override
	public void setContent(IoBuffer content) {
		if (content == null) {
			throw new NullPointerException("content");
		}

		String ct = getContentType();
		if (ct != null
				&& ct.toLowerCase()
						.startsWith(
								HttpHeaderConstants.VALUE_URLENCODED_FORM
										.toLowerCase())) {
			content.mark();
			try {
				setParameters(content.getString(HttpCodecUtils.DEFAULT_CHARSET
						.newDecoder()));
			} catch (CharacterCodingException e) {
				throw new IllegalArgumentException(
						"Failed to decode the url-encoded content.", e);
			} finally {
				content.reset();
			}
		}
		super.setContent(content);
	}

	/**
	 * Thread-local DateFormat for old-style cookies
	 */
	private static final ThreadLocal<DateFormat> EXPIRY_FORMAT_LOACAL = new ThreadLocal<DateFormat>() {
		@Override
		protected DateFormat initialValue() {
			SimpleDateFormat format = new SimpleDateFormat(
					"EEE, dd-MMM-yyyy HH:mm:ss z", Locale.US);
			format.setTimeZone(TimeZone
					.getTimeZone(HttpCodecUtils.DEFAULT_TIME_ZONE_NAME));
			return format;
		}

	};

	/**
	 * A date long long ago, formatted in the old style cookie expire format
	 */
	private static final String EXPIRED_DATE = getFormattedExpiry(0);

	private static String getFormattedExpiry(long time) {
		DateFormat format = EXPIRY_FORMAT_LOACAL.get();
		return format.format(new Date(time));
	}

	public void normalize() {
		// Encode parameters.
		Map<String, List<String>> params = getParameters();
		if (!params.isEmpty()) {
			try {
				boolean first = true;
				StringBuilder buf = new StringBuilder();
				for (Map.Entry<String, List<String>> e : params.entrySet()) {
					if (e.getValue().isEmpty()) {
						continue;
					}

					for (String v : e.getValue()) {
						if (!first) {
							buf.append('&');
						}

						buf.append(URLEncoder.encode(e.getKey(),
								HttpCodecUtils.DEFAULT_CHARSET_NAME));
						buf.append('=');
						buf.append(URLEncoder.encode(v,
								HttpCodecUtils.DEFAULT_CHARSET_NAME));
						first = false;
					}
				}

				if (buf.length() > 0) {
					String uri = getRequestUri().toString();
					int queryIndex = uri.indexOf('?');
					switch (getMethod()) {
					case POST:
						if (queryIndex >= 0) {
							setRequestUri(new URI(uri.substring(0, queryIndex)));
						}
						IoBuffer content = IoBuffer.allocate(buf.length());
						content.put(buf.toString().getBytes(
								HttpCodecUtils.US_ASCII_CHARSET_NAME));
						content.flip();
						setContent(content);
						setHeader(HttpHeaderConstants.KEY_CONTENT_TYPE,
								HttpHeaderConstants.VALUE_URLENCODED_FORM);
						break;
					default:
						if (queryIndex >= 0) {
							setRequestUri(new URI(uri.substring(0,
									queryIndex + 1) + buf));
						} else {
							setRequestUri(new URI(uri + '?' + buf));
						}
					}
				}
			} catch (Exception e) {
				throw (InternalError) new InternalError("Unexpected exception.")
						.initCause(e);
			}
		}

		// Encode Cookies
		Set<Cookie> cookies = getCookies();
		if (!cookies.isEmpty()) {
			// Clear previous values.
			removeHeader(HttpHeaderConstants.KEY_COOKIE);

			// And encode.
			for (Cookie c : cookies) {
				StringBuilder buf = new StringBuilder();
				buf.append(c.getName());
				buf.append('=');
				buf.append(c.getValue());
				if (c.getVersion() > 0) {
					buf.append("; version=");
					buf.append(c.getVersion());
				}
				if (c.getPath() != null) {
					buf.append("; path=");
					buf.append(c.getPath());
				}

				long expiry = c.getMaxAge();
				int version = c.getVersion();
				if (expiry >= 0) {
					if (version == 0) {
						String expires = expiry == 0 ? EXPIRED_DATE
								: getFormattedExpiry(System.currentTimeMillis()
										+ 1000 * expiry);
						buf.append("; Expires=");
						buf.append(expires);
					} else {
						buf.append("; max-age=");
						buf.append(c.getMaxAge());
					}
				}

				if (c.isSecure()) {
					buf.append("; secure");
				}
				if (c.isHttpOnly()) {
					buf.append("; HTTPOnly");
				}

				buf.append(';');

				addHeader(HttpHeaderConstants.KEY_COOKIE, buf.toString());
			}
		}

		// Add the Host header.
		if (!containsHeader(HttpHeaderConstants.KEY_HOST)) {
			URI uri = getRequestUri();
			String host = uri.getHost();
			if (host != null) {
				if ((uri.getScheme().equalsIgnoreCase("http")
						&& uri.getPort() != 80 && uri.getPort() > 0)
						|| (uri.getScheme().equalsIgnoreCase("https")
								&& uri.getPort() != 443 && uri.getPort() > 0)) {
					setHeader(HttpHeaderConstants.KEY_HOST,
							host + ':' + uri.getPort());
				} else {
					setHeader(HttpHeaderConstants.KEY_HOST, host);
				}
			}
		}

		// Set Content-Length.
		if (!containsHeader(HttpHeaderConstants.KEY_TRANSFER_CODING)) {
			IoBuffer content = getContent();
			int contentLength = content == null ? 0 : content.remaining();
			setHeader(HttpHeaderConstants.KEY_CONTENT_LENGTH,
					String.valueOf(contentLength));
		}
	}
}
